/*****************************************************************************
 *                     Projet de HCR : Annuaire distribué                    *
 *                      Guillaume MONTAVON (Master 2 RIM)                    *
 *                                                                           *
 *                  Gestion du client : connexion au serveur,                *
 *                connexion aux autres clients, gestion du menu, ...         *
 *****************************************************************************/

#include "client.h"

void init(void) {
#ifdef WIN32
	WSADATA wsa;
	int err = WSAStartup(MAKEWORD(2, 2), &wsa);
	if(err < 0) {
		puts("WSAStartup failed !");
		exit(EXIT_FAILURE);
	}
#endif
}

void end(void) {
#ifdef WIN32
	WSACleanup();
#endif
}

// Création de la socket UDP qui permettra d'envoyer un message en broadcast
int connexionUDPServeur(const char *address, int port, SOCKADDR_IN *sin, int broadcast) {
	
	SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
	struct hostent *hostinfo;
	
	if(sock == INVALID_SOCKET) {
		perror("socket()");
		//printf("\nDesole, je ne peux pas creer la socket du a l'erreur : %d\n",WSAGetLastError());
		exit(errno);
	}
	
	if (broadcast == 1) {
		if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof broadcast) == -1) {
			perror("setsockopt (SO_BROADCAST)");
			exit(1);
		}
	}
	
	hostinfo = gethostbyname(address);
	if (hostinfo == NULL) {
		fprintf (stderr, "Unknown host %s.\n", address);
		exit(EXIT_FAILURE);
	}
	
	sin->sin_addr = *(IN_ADDR *) hostinfo->h_addr;
	sin->sin_port = htons(port);
	sin->sin_family = AF_INET;
	
	return sock;
}

// Création de la socket UDP qui permettra de recevoir un message du serveur
int connexionUDPServeurRecoit(int port) {
	SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
	SOCKADDR_IN sin = { 0 };
	
	if(sock == INVALID_SOCKET) {
		perror("socket()");
		exit(errno);
	}
	int opt = 1;
	if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt) < 0) {
		perror("setsockopt (SO_REUSEADDR)");
		exit(1);
	}

	sin.sin_addr.s_addr = htonl(INADDR_ANY);
	sin.sin_port = htons(port);
	sin.sin_family = AF_INET;
	
	if(bind(sock,(SOCKADDR *) &sin, sizeof sin) == SOCKET_ERROR) {
		perror("bind()");
		exit(errno);
	}
	
	return sock;
}

// On envoie une requête en broadcast à tout notre réseau local, le serveur nous répondra, ainsi on pourra connaître l'adresse IP de celui-ci
int demanderAdresseServeur(char* adresseServeur) {
	
	char adresseBroadcast[] = "255.255.255.255";
	
	SOCKADDR_IN sin = { 0 };
	SOCKET sock = connexionUDPServeur(adresseBroadcast, PORT_SERVEUR, &sin, 1);
	char buffer[] = "clientgmontavon";
	
	/* send our name */
	envoyerUDP(sock, &sin, buffer);
	
	fprintf(stdout, "Le client envoie %s en broadcast (%s)\n", buffer, adresseBroadcast);
	fprintf(stdout, "Recherche du serveur ... \n");
	
	fermerConnexion(sock);
	
	sock = connexionUDPServeurRecoit(PORT_SERVEUR);
	
	int n = recevoirUDP(sock, &sin, buffer);
	/* server down */
	if(n == 0) {
		fprintf(stdout, "Server disconnected !\n");
		return -1;
	}
	
	strcpy(adresseServeur,inet_ntoa(((const SOCKADDR_IN*)&sin)->sin_addr)); //obtenir l'adresse du serveur
	
	if(strcmp(buffer, "serveurgmontavon") == 0)
		fprintf(stdout, "Client reçoit %s du serveur(%s)\n", buffer, adresseServeur);
	else {
		fprintf(stderr, "Le serveur qui a renvoyé la réponse n'est pas le mien (%s)\n", adresseServeur);
		strcpy(adresseServeur, "");
	}
	
	fermerConnexion(sock);
	
	return 1;
}

// Création d'une socket pour se connecter à un serveur
int initConnexion(const char *address, int port) {
	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
	SOCKADDR_IN sin = { 0 };
	struct hostent *hostinfo;
	
	if(sock == INVALID_SOCKET) {
		perror("socket()");
		exit(errno);
	}
	
	hostinfo = gethostbyname(address);
	if (hostinfo == NULL) {
		fprintf (stderr, "Unknown host %s.\n", address);
		exit(EXIT_FAILURE);
	}
	
	sin.sin_addr = *(IN_ADDR *) hostinfo->h_addr;
	sin.sin_port = htons(port);
	sin.sin_family = AF_INET;
	
	if(connect(sock,(SOCKADDR *) &sin, sizeof(SOCKADDR)) == SOCKET_ERROR) {
		perror("connect()");
		exit(errno);
	}
	
	return sock;
}

// Création d'une socket créer un serveur
int initConnexionServeur(int port) {
	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
	SOCKADDR_IN sin = { 0 };
	
	if(sock == INVALID_SOCKET) {
		perror("socket()");
		exit(errno);
	}
	
	sin.sin_addr.s_addr = htonl(INADDR_ANY);
	sin.sin_port = htons(port);
	sin.sin_family = AF_INET;
	
	// Mode SO_REUSEADDR pour eviter les: "address already in use"
	int on = 1;
	int err = setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof( on));
	if ( err < 0 )
		perror("serveur : erreur setsockopt");
	
	
	if(bind(sock,(SOCKADDR *) &sin, sizeof sin) == SOCKET_ERROR) {
		perror("bind()");
		exit(errno);
	}
	
	int max_client = 1;	
	
	if(listen(sock, max_client) == SOCKET_ERROR) {
		perror("listen()");
		exit(errno);
	}
	
	return sock;
}

// Ferme la connexion d'une socket
void fermerConnexion(int sock) {
	closesocket(sock);
}

// Recevoir un message du serveur via le protocole TCP
int recevoirServeurTCP(SOCKET sock, char *buffer) {
	int n = 0;
	
	if((n = recv(sock, buffer, BUF_SIZE - 1, 0)) < 0) {
		perror("recv()");
		shutdown(sock, 2);
		n = 0;
		//exit(errno);
	}
	
	buffer[n] = '\0';
	
	return n;
}

// Envoyer un message au serveur via le protocole TCP
void envoyerServeurTCP(SOCKET sock, const char *buffer) {
	if(send(sock, buffer, strlen(buffer), 0) < 0) {
		perror("send()");
		shutdown(sock, 2);
		exit(errno);
	}
}

// Recevoir un message via le protocole UDP
int recevoirUDP(SOCKET sock, SOCKADDR_IN *sin, char *buffer) {
	int n = 0;
	size_t sinsize = sizeof *sin;
	
	if((n = recvfrom(sock, buffer, BUF_SIZE - 1, 0, (SOCKADDR *) sin, &sinsize)) < 0) {
		perror("recvfrom()");
		//printf("\nDesole, je ne peux pas creer la socket du a l'erreur : %d\n",WSAGetLastError());
		exit(errno);
	}
	
	buffer[n] = '\0';
	
	return n;
}

// Envoyer un message via le protocole UDP
void envoyerUDP(SOCKET sock, SOCKADDR_IN *sin, const char *buffer) {
	if(sendto(sock, buffer, strlen(buffer), 0, (SOCKADDR *) sin, sizeof *sin) < 0) {
		perror("sendto()");
		//printf("\nDesole, je ne peux pas creer la socket du a l'erreur : %d\n",WSAGetLastError());
		exit(errno);
	}
}

// Envoie une requête au serveur Java d'annuaire, afin d'enregistrer tous les nom qui se trouve dans notre annuaire
int enregistrementAnnuaireServeur(SOCKET sock) {
	
	char message[TAILLE_TEL] = "enregistrement\n";
	
	envoyerServeurTCP(sock, message);
	
	int i;
	for(i = 0; i < tailleAnnuaire(annu); i++) {
		strcpy(message, getPersonneById(annu, i)->nom);
		strcat(message, "\n");
		envoyerServeurTCP(sock, message);
	}
	
	strcpy(message, "termine\n");
	
	envoyerServeurTCP(sock, message);
	
	return 1;
	
}

// Envoie une requête au serveur Java d'annuaire, afin d'ajouter un nom dans sa base
int ajouterAnnuaireServeur(SOCKET sock, char *nom) {
	
	char message[TAILLE_TEL] = "enregistrement\n";
	
	envoyerServeurTCP(sock, message);

	strcpy(message, nom);
	strcat(message, "\n");
	envoyerServeurTCP(sock, message);
	
	strcpy(message, "termine\n");
	
	envoyerServeurTCP(sock, message);
	
	return 1;
	
}

// Envoie une requête au serveur Java d'annuaire, afin de supprimer de sa base le nom demandé 
int supprimerAnnuaireServeur(SOCKET sock, char *nom) {
	
	char message[TAILLE_TEL] = "suppression\n";
	
	envoyerServeurTCP(sock, message);

	strcpy(message, nom);
	strcat(message, "\n");
	envoyerServeurTCP(sock, message);
	
	char confirmation[BUF_SIZE] = "";
	
	recevoirServeurTCP(sock, confirmation);
	
	if(strcmp(confirmation, "suppression") == 0) {
		fprintf(stdout, "Personne également supprimé sur le serveur");
		return 1;
	}
	else {
		fprintf(stderr, "Erreur lors de la suppression de la personne sur le serveur\n");
		return -1;
	}
	
	return 1;
	
}

// Demande au serveur Java d'annuaire, l'adresse IP du client qui connait le nom demandé 
int demandeServeurIPClient(SOCKET sock, char *nom, char *ipClient) {

	char message[TAILLE_TEL] = "demande\n";
	envoyerServeurTCP(sock, message);
	
	strcpy(message, nom);
	strcat(message, "\n");
	
	envoyerServeurTCP(sock, message);
	
	recevoirServeurTCP(sock, ipClient);
	
	if(strcmp(ipClient, "inconnu") != 0) {
		fprintf(stdout, "ip du client qui connait %s : %s\n", nom, ipClient);
		return 1;
	}
	else {
		fprintf(stderr, "Aucun client ne connait ce nom\n");
		return -1;
	}
	
}

// Demande à un autre client le numéro associé avec le nom recherché
int demandeClientNumero(const char *adresseClient, const char *nomDemande, char *telAssocie) {
	
	int numPort = PORT_CLIENT;
	if(numClient == 1)
		numPort += 2; 
	else if(numClient == 2)
		numPort += 1;
	
	SOCKET sockClient = initConnexion(adresseClient, numPort);
	
	envoyerServeurTCP(sockClient, nomDemande);
	
	fprintf(stdout, "On demande %s à un autre client (%s)\n", nomDemande, adresseClient);
	
	int n = recevoirServeurTCP(sockClient, telAssocie);
	if(n == 0) {
		fprintf(stdout, "Server disconnected !\n");
		return -1;
	}
	
	fermerConnexion(sockClient);
	
	return 1;
	
}

//fonction appelé par un thread pour envoyer aux autres client le numéro qu'il demande
void* lanceThreadEnvoieNum(void* data) {
	
	while(1)
		envoieClientNumero();
	
	return NULL;

}

//Attend un message de n'importe quel client puis lui renvoie le numéro associé
int envoieClientNumero() {
	
	char nomDemande[TAILLE_TEL];
	
	SOCKET sockServeur = initConnexionServeur(PORT_CLIENT + numClient);
	
	SOCKET csock = accept(sockServeur, NULL, NULL);
	if(csock < 0)
		fprintf(stderr, "erreur accept\n");
	
	int n = recevoirServeurTCP(csock, nomDemande);
	if(n == 0) {
		fprintf(stderr, "Server disconnected !\n");
		return -1;
	}
	
	char numTel[TAILLE_TEL];
	
	obtenirNumTel(annu, nomDemande, numTel); // if > 0
	
	envoyerServeurTCP(csock, numTel);
	
	fprintf(stderr, "Le client envoie %s au client\n", numTel);
	
	shutdown(csock, 2);
	shutdown(sockServeur, 2);
	fermerConnexion(csock);
	fermerConnexion(sockServeur);
	return 1;

}

// Libère le buffer du scanf
void clearScanf(void) {
	char c;
	while((c = getchar()) != EOF && c != '\n'); 
	return;
}

// Affichage d'un menu qui donne accès à l'utilisateur à de nombreuses fonctionnalités 
// afin de se connecter au serveur, ajouter des noms ou encore demander un numéro
void menu() { 
	
	int retour = 1;
	while(retour > 0) {
		afficherMenu();
		retour = actionsMenu();
	}
}

// Affichage simple du menu avec les differentes options
void afficherMenu() {
	
	fprintf(stdout, "============== Menu =============\n\n");
	fprintf(stdout, "Que souhaitez-vous faire?\n");
	fprintf(stdout, "  1. Afficher notre annuaire\n");
	fprintf(stdout, "  2. Chercher le serveur\n");
	if(sockServeur != -1)
		fprintf(stdout, "  3. Demander le numéro d'une personne\n");
	fprintf(stdout, "  4. Ajouter une personne dans notre annuaire\n");
	fprintf(stdout, "  5. Supprimer une personne dans notre annuaire\n");
	fprintf(stdout, "  6. Importer un fichier contenant l'annuaire\n");
	fprintf(stdout, "  7. Exporter notre annuaire dans un fichier\n");
	fprintf(stdout, "  8. Quitter\n");
	fprintf(stdout, "=================================\n");
	
}

// Gère les differentes actions qui peuvent être demandées par l'utilisateur
int actionsMenu() {
	
	int reponse = 0;
	int nbCar = 0;
	
	nbCar = scanf("%d", &reponse);
	clearScanf();
	
	switch(reponse) {
			//Afficher notre annuaire
		case AFFICHER :
			afficherAnnuaire(annu);
			break;
			//Chercher le serveur
		case CHERCHER_SERVEUR :
			{
			
			char adresseServeur[TAILLE_TEL] = "";
			
			if(numClient != 0)
				strcpy(adresseServeur, "127.0.0.1");
			else
				demanderAdresseServeur(adresseServeur);
			
			//si on a trouvé le serveur
			if(strcmp(adresseServeur, "") != 0) {
				fprintf(stdout, "\nConnexion au serveur, port %d\n\n", PORT_SERVEUR + numClient);
				sockServeur = initConnexion(adresseServeur, PORT_SERVEUR + numClient);
			
				enregistrementAnnuaireServeur(sockServeur);
			}
			
			}
			break;
			//Demander un numéro
		case DEMANDER_NUM :
			{
			char ipClient[BUF_SIZE] = "";
			char nom[TAILLE_TEL];
			char tel[TAILLE_TEL];
			fprintf(stdout, "Nom demandé : ");
			nbCar = scanf("%[^\n]", nom);
			if(nbCar <= 0)
				fprintf(stderr, "chaine de caractères incorrect");
			clearScanf();
			
			fprintf(stdout, "Recherche ...\n");
			
			if(demandeServeurIPClient(sockServeur, nom, ipClient) > 0) {
				demandeClientNumero(ipClient, nom, tel);
				fprintf(stdout, "Le numéro de tel de %s est : %s\n", nom, tel);
			}
			}
			break;
			//Ajouter une personne dans notre annuaire et le signaler au serveur
		case AJOUTER :
			{
			char nom[TAILLE_TEL];
			char tel[TAILLE_TEL];
			fprintf(stdout, "Nom à ajouter : ");
			nbCar = scanf("%[^\n]", nom);
			if(nbCar <= 0)
				fprintf(stderr, "chaine de caractères incorrect");
			clearScanf();
			
			fprintf(stdout, "Numéro de téléphone : ");
			nbCar = scanf("%s", tel);
			if(nbCar <= 0)
				fprintf(stderr, "chaine de caractères incorrect");
			clearScanf();
			
			annu = ajouterAnnuaire(annu, nom, tel);
			
			//si on est connecté au serveur
			if(sockServeur != -1)
				ajouterAnnuaireServeur(sockServeur, nom);
			
			fprintf(stdout, "Ajout de '%s' (%s) effectuee\n", nom, tel);
			}
			break;
			//Supprimer une personne dans notre annuaire et le signaler au serveur
		case SUPPRIMER :
			{
			char nom[TAILLE_TEL];
			fprintf(stdout, "Nom à supprimer : ");
			nbCar = scanf("%[^\n]", nom);
			if(nbCar <= 0)
				fprintf(stderr, "chaine de caractères incorrect");
			clearScanf();
			
			if(rechercherPersonne(annu, nom) != NULL) {
				supprimerPersonne(annu, nom);
				
				//si on est connecté au serveur
				if(sockServeur != -1)
					supprimerAnnuaireServeur(sockServeur, nom);
				
				fprintf(stdout, "Suppression de '%s' effectuee\n", nom);
			}
			else
				fprintf(stderr, "La personne '%s' n'existe pas dans votre annuaire, impossible de la supprimer\n", nom);
			}
			break;
			//Importer un fichier contenant l'annuaire
		case IMPORTER :
			fprintf(stdout, "Veuillez entrer le chemin du fichier contenant l'annuaire : ");
			nbCar = scanf("%s", fichierAnnuaire);
			if(nbCar <= 0)
				fprintf(stderr, "chaine de caractères incorrect");
			clearScanf();
			
			annu = importerAnnuaire(annu, fichierAnnuaire);
			
			break;
			//Exporter notre annuaire dans un fichier
		case EXPORTER :
			if(exporterAnnuaire(annu, fichierAnnuaire) > 0)
				fprintf(stdout, "Exportation dans le fichier '%s' effectuee\n", fichierAnnuaire);
			else
				fprintf(stderr, "Erreur lors du chargement du fichier %s : ", fichierAnnuaire);
			break;
			//Quitter
		case QUITTER :
			return -1;
			break;
			//mauvais choix
		default :
			fprintf(stderr, "Erreur lors de la saisie de votre choix, ");
			fprintf(stderr, "Veuillez recommencer.\n");
	}
	
	return 1;

}

//fonction principale qui est exécuté au lancement du programme
int main(int argc, char **argv) {

	init();
	
	if(argc > 1)
		numClient = atoi(argv[1]);
	
	fprintf(stdout, "################################################\n");
	fprintf(stdout, "############ Démarrage du client ###############\n");
	fprintf(stdout, "################################################\n\n");
	
	pthread_t thread;// On crée un thread
	pthread_create(&thread, NULL, lanceThreadEnvoieNum, NULL);// Permet d'exécuter la fonction lanceThreadEnvoieNum en parallèle
	
	//affichage du menu
	menu();
	
	
	fprintf(stdout, "\nDéconnexion du serveur\n");
	fermerConnexion(sockServeur);
	
	
	pthread_cancel(thread);
	//pthread_exit(NULL);
	
	
	fprintf(stdout, "\nAttend la fin du thread\n\n");
	// Attend la fin du thread créé
	pthread_join(thread, NULL);
	
	effacerAnnuaire(annu); // Libère les ressources, nous verrons cette fonction plus tard.
	
	fprintf(stdout, "\nFin du programme\n\n");
	
	end();
	
	return EXIT_SUCCESS;
}
